package Undo
{
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.events.TextEvent;
	
	import mx.core.UIComponent;

	
	public class UndoStack
	{
		public var limit:int;
		protected var stack:Array;
		protected var pointer:int;
		protected static var adapterFactories:Array;
		protected var listened:Array;
		protected var ignoreKey:Boolean;
		
		public function UndoStack(limit:int = 0)
		{
			UndoStack.registerAdapterFactory(null); // make sure the default adapters are registered
			
			this.limit = limit;
			this.listened = new Array();
			this.clear();
		}
		
		public function clear() : void
		{
			this.stack = new Array();
			this.pointer = -1;
			for (var i:int = this.listened.length-1; i >= 0; --i) this.unlisten(this.listened[i]);
			this.listened = new Array();
			this.ignoreKey = false;
		}
		
		public static function registerAdapterFactory(adapterFactory:Function) : void
		{
			if (!UndoStack.adapterFactories) UndoStack.adapterFactories = [TextAreaUndoAdapter.listenTo, ComboBoxUndoAdapter.listenTo];
			if (adapterFactory) UndoStack.adapterFactories.push(adapterFactory);
		}
		
		public function listenTo(object:UIComponent) : Boolean
		{
			if (!object) return false;
			
			object.addEventListener(KeyboardEvent.KEY_DOWN, this.keyListener);
			object.addEventListener(TextEvent.TEXT_INPUT, this.cancelKey, true);
			
			var a:IUndoAdapter = null;
			for (var i:int = 0; !a && i != UndoStack.adapterFactories.length; ++i) 
				a = (UndoStack.adapterFactories[i] as Function)(object, this) as IUndoAdapter;
			if (!a) 
			{
				object.removeEventListener(KeyboardEvent.KEY_DOWN, this.keyListener);
				object.removeEventListener(TextEvent.TEXT_INPUT, this.cancelKey);
			}
			else this.listened.push(a);
			return a != null;
		}
		
		public function unlistenTo(object:UIComponent) : void
		{
			for (var a:IUndoAdapter, i:int = this.listened.length - 1; i >= 0; --i) if ((a = this.listened[i] as IUndoAdapter).getListened() == object) 
			{
				this.listened.splice(i, 1);
				this.unlisten(a);
				for (var j:int = this.stack.length - 1; j >= 0; --j) if ((this.stack[j] as UndoEvent).currentTarget == a) 
				{
					this.stack.splice(j, 1);
					if (this.pointer >= j) --this.pointer;
				}
				return;
			}
		}
		
		private function unlisten(adapter:IUndoAdapter) : void
		{
			var object:UIComponent = adapter.getListened();
			object.removeEventListener(KeyboardEvent.KEY_DOWN, this.keyListener);
			object.removeEventListener(TextEvent.TEXT_INPUT, this.cancelKey);
		}

		protected function keyListener(event:KeyboardEvent) : void
		{
			//trace("keyListener: event="+event.type+", shiftKey="+event.shiftKey+", ctrlKey="+event.ctrlKey+", altKey="+event.altKey+", keyCode="+event.keyCode+", charCode="+event.charCode+", keyLocation="+event.keyLocation);
   			switch (event.ctrlKey? event.charCode : -1)
   			{
   				case 26: case 122: if (event.shiftKey) this.redo(); else this.undo(); this.ignoreKey = true; break;
   				case 25: case 121: this.redo(); this.ignoreKey = true; break;
   			}
		}
		
		protected function cancelKey(event:Event) : void
		{
			if (this.ignoreKey) { event.preventDefault(); event.stopImmediatePropagation(); }
			this.ignoreKey = false;
		}
				
		//-----------------------------------------------------------------------------------------------------------

		public function push(event:UndoEvent) : void
		{
			this.stack.length = this.pointer + 1;
			this.stack.push(event);
			var d:int = this.limit > 0? this.stack.length - this.limit : 0;
			if (d > 0) this.stack.splice(0, d);
			this.pointer = this.stack.length - 1;
		}
		
		public function tryPop() : UndoEvent
		{
			if (this.stack.length && this.pointer + 1 == this.stack.length)
			{
				--this.pointer;
				return this.stack.pop();
			}
			else return null;
		}
		
		public function undo() : Boolean
		{
			while (this.pointer >= 0)
			{
				var event:UndoEvent = this.stack[this.pointer--] as UndoEvent;
				if ((event.currentTarget as IUndoAdapter).reverse(event.after, event.before)) return true;
				else this.unlistenTo(event.target as UIComponent);
			} 
			return false;
		}
		
		public function redo() : Boolean
		{
			while (this.pointer + 1 < this.stack.length)
			{
				var event:UndoEvent = this.stack[++this.pointer];
				if ((event.currentTarget as IUndoAdapter).reverse(event.before, event.after)) return true;
				else this.unlistenTo(event.target as UIComponent);
			} 
			return false;
		}
	}
}
